package org.chispa.api.visitors;

import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.chispa.api.domain.DomainCryptoListener;
import org.chispa.api.domain.DomainParameter;
import org.chispa.api.domain.DomainRetriever;
import org.chispa.api.domain.DomainRetrieverFactory;
import org.chispa.api.query.ParamWrapper;
import org.chispa.api.util.BadSyntaxException;
import org.chispa.api.util.BinningNumber;
import org.chispa.api.util.BinningNumber.IdPair;
import org.chispa.api.util.ExpressionLanguage;
import org.chispa.api.util.NoSuitableVisitorException;
import org.chispa.api.util.RelationalSymbol;

import com.google.common.base.Joiner;

public class NumericParamVisitor extends SingleParamVisitor<BinningNumber, Long> implements DomainCryptoListener {
	
	private String algo="HmacSHA1";
	
	NumericParamVisitor(Cipher blockCipher, SecretKeySpec spec,
			IvParameterSpec ivSpec) {
		super(blockCipher, spec, ivSpec);
	}
	
	NumericParamVisitor(Cipher blockCipher, SecretKeySpec spec,
			IvParameterSpec ivSpec,String algo) {
		this(blockCipher, spec, ivSpec);
		this.algo=algo;
		
	}

	@Override
	public String visitEncrypt(ParamWrapper<BinningNumber,Long, String> wrap) {
		Number nat=wrap.getWrappedParam().getNative();
		try{
			byte[]parameter=nat.toString().getBytes();
			return doEncrypt(parameter);
		} catch (Exception e) {
			System.err.println(e.getLocalizedMessage());
			return null;
		}
	}

	@Override
	public Long visitHash(ParamWrapper<BinningNumber, Long,String> wrap)
			throws NoSuitableVisitorException, NoSuchAlgorithmException {
		Mac mac=Mac.getInstance(algo);
		return wrap.getWrappedParam().getEncryptedOrdinal(mac, spec);
		
	}

	@Override
	public String visitHashExpr(ParamWrapper<String, String,String> wrap,Map<String,String>params)
			throws BadSyntaxException, NoSuitableVisitorException {
		Set<String>keys=params.keySet();
		StringBuilder build=new StringBuilder();
		DomainRetriever dr=DomainRetrieverFactory.getInstance(this);
		HashMap<String,BinningNumber> defNums=new HashMap<String,BinningNumber>();
		
		for(String str:keys)
		{
			String ns=params.get(str);
			defNums.put(str,dr.createDefaultNumber(ns));
		}
		Scanner scan=new Scanner(wrap.getWrappedParam());
		while(scan.hasNext())
		{
			String token=scan.next();
			if(ExpressionLanguage.isParameter(token))
			{
				token=onParam(token,defNums,scan);
			}
			else if(isNumeric(token))
			{
				token=onNumeric(token, defNums, scan);
			}
			build.append(token+" ");
		}
		return build.toString().trim();
	}

	private boolean isNumeric(String in) {

		try {
			Long.parseLong(in);
		} catch (NumberFormatException ex) {
			try {
				Double.parseDouble(in);
			} catch (NumberFormatException ex2) {
				return false;
			}  
			return true;
		}  
		return true;
	}
	
	private String onParam(String param,Map<String,BinningNumber> defNums,Scanner scan) throws BadSyntaxException, NoSuitableVisitorException
	{
		String rel=scan.next();
		String nextPar=scan.next();
		RelationalSymbol symb=RelationalSymbol.fromString(rel);
		if(symb==null)
			throw new BadSyntaxException("Parameters must be followed by relational symbol!");
		try {
			if(ExpressionLanguage.isParameter(nextPar))
			{
				return twoParams(param,symb,nextPar, defNums);
			}
			return oneParam(param,symb,nextPar, defNums);
		} catch (NoSuchAlgorithmException e) {
			throw new NoSuitableVisitorException(e);
		}
	}
	
	private String onNumeric(String param,Map<String,BinningNumber> defNums,Scanner scan) throws BadSyntaxException, NoSuitableVisitorException
	{
		String rel=scan.next();
		String nextPar=scan.next();
		RelationalSymbol symb=RelationalSymbol.fromString(rel);
		if(symb==null)
			throw new BadSyntaxException("Numbers must be followed by relational symbol!");
		try {
			return oneParam(param,symb.doSwitch(),nextPar, defNums);
		} catch (NoSuchAlgorithmException e) {
			throw new NoSuitableVisitorException(e);
		}
	}
	
	private String twoParams(String first,RelationalSymbol symb,String second,Map<String,BinningNumber> defNums) throws NoSuchAlgorithmException, NoSuitableVisitorException
	{
		BinningNumber firstNum=defNums.get(first);
		BinningNumber secondNum=defNums.get(second);
		Mac mac=Mac.getInstance(algo);
		List<IdPair> values=new ArrayList<IdPair>();
		switch(symb){
		case LESS:
			values=firstNum.getOverlappingLess(secondNum, mac, spec);
			break;
		case GREATER:
			values=firstNum.getOverlappingMore(secondNum, mac, spec);
			break;
		case GRT_OR_EQ:
			values=firstNum.getOverlappingMoreOrEqual(secondNum, mac, spec);
			break;
		case LESS_OR_EQ:
			values=firstNum.getOverlappingLessOrEqual(secondNum, mac, spec);
			break;
		case EQUALS:
			values=firstNum.getOverlappingEquals(secondNum, mac, spec);
			break;
		case NOT_EQUAL:
			values=firstNum.getOverlappingNotEqual(secondNum, mac, spec);
			break;
		}
		String joined=Joiner.on(" , ").skipNulls().join(values);
		return first+":"+second+" { "+joined+" }";
	}
	
	private String oneParam(String param,RelationalSymbol symb,String value,Map<String,BinningNumber> defNums) throws NoSuchAlgorithmException, BadSyntaxException, NoSuitableVisitorException
	{
		BinningNumber firstNum=defNums.get(param);
		Mac mac=Mac.getInstance(algo);
		if(!isNumeric(value))
			throw new BadSyntaxException("Relational operators can only be placed between two variables or a number and a variable!");
		Number numValue;
		try{
			numValue=Long.parseLong(value);
		} catch (NumberFormatException ex) {
			numValue=Double.parseDouble(value);
		}
		firstNum=firstNum.cloneFromDefault(numValue);
		Long[]indexes=new Long[0];
		switch(symb){
		case LESS:
			indexes=firstNum.getEncryptedOrdinalLess(mac, spec);
			break;
		case GREATER:
			indexes=firstNum.getEncryptedOrdinalGreater(mac, spec);
			break;
		case GRT_OR_EQ:
			indexes=firstNum.getEncryptedOrdinalGreaterOrEqual(mac, spec);
			break;
		case LESS_OR_EQ:
			indexes=firstNum.getEncryptedOrdinalLessOrEqual(mac, spec);
			break;
		case EQUALS:
			indexes=new Long[]{firstNum.getEncryptedOrdinal(mac, spec)};
			break;
		case NOT_EQUAL:
			indexes=firstNum.getEncryptedOrdinalNotEqual(mac, spec);
			break;
		}
		String joined=Joiner.on(" , ").skipNulls().join(indexes);
		return param+" { "+joined+" }";
	}

	@Override
	public String visitDecrypt(String encrypted) {
			return doDecrypt(encrypted);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T extends Number> T onNumber(String enc, T type) {
		String dec=visitDecrypt(enc);
		return (T) DomainParameter.parseNumber(dec, type.getClass());
	}

	@Override
	public Integer onBucketNumber(String enc) {
		return Integer.parseInt(visitDecrypt(enc));
	}

}
